<?php
/**
 * 2007-2020 PrestaShop SA and Contributors
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Open Software License (OSL 3.0)
 * that is bundled with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * https://opensource.org/licenses/OSL-3.0
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@prestashop.com so we can send you a copy immediately.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer
 * versions in the future. If you wish to customize PrestaShop for your
 * needs please refer to https://www.prestashop.com for more information.
 *
 * @author    PrestaShop SA <contact@prestashop.com>
 * @copyright 2007-2020 PrestaShop SA and Contributors
 * @license   https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
 * International Registered Trademark & Property of PrestaShop SA
 */

/**
 * @todo : Create typed exception for more finer errors check
 */
class WebserviceOutputBuilderCore
{
    /**
     * @var int constant
     */
    const VIEW_LIST = 1;
    const VIEW_DETAILS = 2;

    protected $wsUrl;
    protected $output;

    /** @var WebserviceOutputInterface|WebserviceOutputXML|WebserviceOutputJSON */
    public $objectRender;
    protected $wsResource;
    protected $depth = 0;
    protected $schemaToDisplay;
    protected $fieldsToDisplay;
    protected $specificFields = [];
    protected $virtualFields = [];
    protected $statusInt;
    protected $wsParamOverrides;

    protected static $_cache_ws_parameters = [];

    /* Header properties */
    protected $headerParams = [
        'Access-Time' => 0,
        'X-Powered-By' => 0,
        'PSWS-Version' => 0,
        'Content-Type' => 0,
    ];

    /**
     * @var string Status header sent at return
     */
    protected $status;

    public function __construct($ws_url)
    {
        $this->statusInt = 200;
        $this->status = $_SERVER['SERVER_PROTOCOL'] . ' 200 OK';
        $this->wsUrl = $ws_url;
        $this->wsParamOverrides = [];
    }

    /**
     * Set the render object for set the output format.
     * Set the Content-type for the http header.
     *
     * @param WebserviceOutputInterface $obj_render
     * @throw WebserviceException if the object render is not an instance of WebserviceOutputInterface
     *
     * @return WebserviceOutputBuilder
     *
     * @throws WebserviceException
     */
    public function setObjectRender(WebserviceOutputInterface $obj_render)
    {
        if (!$obj_render instanceof WebserviceOutputInterface) {
            throw new WebserviceException('Obj_render param must be an WebserviceOutputInterface object type', [83, 500]);
        }

        $this->objectRender = $obj_render;
        $this->objectRender->setWsUrl($this->wsUrl);
        if ($this->objectRender->getContentType()) {
            $this->setHeaderParams('Content-Type', $this->objectRender->getContentType());
        }

        return $this;
    }

    /**
     * getter.
     *
     * @return WebserviceOutputInterface
     */
    public function getObjectRender()
    {
        return $this->objectRender;
    }

    /**
     * Need to have the resource list to get the class name for an entity,
     * To build.
     *
     * @param array $resources
     *
     * @return WebserviceOutputBuilder
     */
    public function setWsResources($resources)
    {
        $this->wsResource = $resources;

        return $this;
    }

    /**
     * This method return an array with each http header params for a content.
     * This check each required params.
     *
     * If this method is overrided don't forget to check required specific params (for xml etc...)
     *
     * @return array
     */
    public function buildHeader()
    {
        $return = [];
        $return[] = $this->status;
        foreach ($this->headerParams as $key => $param) {
            $return[] = trim($key) . ': ' . $param;
        }

        return $return;
    }

    /**
     * @param string $key The normalized key expected for an http response
     * @param string $value
     *
     * @return WebserviceOutputBuilder
     *
     * @throws WebserviceException If the key or the value are corrupted (use Validate::isCleanHtml method)
     */
    public function setHeaderParams($key, $value)
    {
        if (!Validate::isCleanHtml($key) || !Validate::isCleanHtml($value)) {
            throw new WebserviceException('the key or your value is corrupted.', [94, 500]);
        }
        $this->headerParams[$key] = $value;

        return $this;
    }

    /**
     * @param string|null $key if null get all header params otherwise the params specified by the key
     * @throw WebserviceException if the key is corrupted (use Validate::isCleanHtml method)
     * @throw WebserviceException if the asked key does'nt exists.
     *
     * @return array|string
     */
    public function getHeaderParams($key = null)
    {
        $return = '';

        if (null !== $key) {
            if (!Validate::isCleanHtml($key)) {
                throw new WebserviceException('the key you write is a corrupted text.', [95, 500]);
            }
            if (!array_key_exists($key, $this->headerParams)) {
                throw new WebserviceException(sprintf('The key %s does\'nt exist', $key), [96, 500]);
            }
            $return = $this->headerParams[$key];
        } else {
            $return = $this->headerParams;
        }

        return $return;
    }

    /**
     * Delete all Header parameters previously set.
     *
     * @return WebserviceOutputBuilder
     */
    public function resetHeaderParams()
    {
        $this->headerParams = [];

        return $this;
    }

    /**
     * @return string the normalized status for http request
     */
    public function getStatus()
    {
        return $this->status;
    }

    public function getStatusInt()
    {
        return $this->statusInt;
    }

    /**
     * Set the return header status.
     *
     * @param int $num the Http status code
     */
    public function setStatus($num)
    {
        $this->statusInt = (int) $num;
        switch ($num) {
            case 200:
                $this->status = $_SERVER['SERVER_PROTOCOL'] . ' 200 OK';

                break;
            case 201:
                $this->status = $_SERVER['SERVER_PROTOCOL'] . ' 201 Created';

                break;
            case 204:
                $this->status = $_SERVER['SERVER_PROTOCOL'] . ' 204 No Content';

                break;
            case 304:
                $this->status = $_SERVER['SERVER_PROTOCOL'] . ' 304 Not Modified';

                break;
            case 400:
                $this->status = $_SERVER['SERVER_PROTOCOL'] . ' 400 Bad Request';

                break;
            case 401:
                $this->status = $_SERVER['SERVER_PROTOCOL'] . ' 401 Unauthorized';

                break;
            case 403:
                $this->status = $_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden';

                break;
            case 404:
                $this->status = $_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found';

                break;
            case 405:
                $this->status = $_SERVER['SERVER_PROTOCOL'] . ' 405 Method Not Allowed';

                break;
            case 500:
                $this->status = $_SERVER['SERVER_PROTOCOL'] . ' 500 Internal Server Error';

                break;
            case 501:
                $this->status = $_SERVER['SERVER_PROTOCOL'] . ' 501 Not Implemented';

                break;
            case 503:
                $this->status = $_SERVER['SERVER_PROTOCOL'] . ' 503 Service Unavailable';

                break;
        }
    }

    /**
     * Build errors output using an error array.
     *
     * @param array $errors
     *
     * @return string output in the format specified by WebserviceOutputBuilder::objectRender
     */
    public function getErrors($errors)
    {
        if (!empty($errors)) {
            if (isset($this->objectRender)) {
                $str_output = $this->objectRender->renderErrorsHeader();
                foreach ($errors as $error) {
                    if (is_array($error)) {
                        $str_output .= $this->objectRender->renderErrors($error[1], $error[0]);
                    } else {
                        $str_output .= $this->objectRender->renderErrors($error);
                    }
                }
                $str_output .= $this->objectRender->renderErrorsFooter();
                $str_output = $this->objectRender->overrideContent($str_output);
            } else {
                $str_output = '<pre>' . print_r($errors, true) . '</pre>';
            }
        }

        return $str_output;
    }

    /**
     * Build the resource list in the output format specified by WebserviceOutputBuilder::objectRender.
     *
     * @param $key_permissions
     *
     * @return string
     */
    public function getResourcesList($key_permissions)
    {
        if (null === $this->wsResource) {
            throw new WebserviceException('You must set web service resource for get the resources list.', [82, 500]);
        }
        $output = '';
        $more_attr = ['shopName' => htmlspecialchars(Configuration::get('PS_SHOP_NAME'))];
        $output .= $this->objectRender->renderNodeHeader('api', [], $more_attr);
        foreach ($this->wsResource as $resourceName => $resource) {
            if (in_array($resourceName, array_keys($key_permissions))) {
                $more_attr = [
                    'xlink_resource' => $this->wsUrl . $resourceName,
                    'get' => (in_array('GET', $key_permissions[$resourceName]) ? 'true' : 'false'),
                    'put' => (in_array('PUT', $key_permissions[$resourceName]) ? 'true' : 'false'),
                    'post' => (in_array('POST', $key_permissions[$resourceName]) ? 'true' : 'false'),
                    'delete' => (in_array('DELETE', $key_permissions[$resourceName]) ? 'true' : 'false'),
                    'head' => (in_array('HEAD', $key_permissions[$resourceName]) ? 'true' : 'false'),
                ];
                $output .= $this->objectRender->renderNodeHeader($resourceName, [], $more_attr);

                $output .= $this->objectRender->renderNodeHeader('description', [], $more_attr);
                $output .= $resource['description'];
                $output .= $this->objectRender->renderNodeFooter('description', []);

                if (!isset($resource['specific_management']) || !$resource['specific_management']) {
                    $more_attr_schema = [
                        'xlink_resource' => $this->wsUrl . $resourceName . '?schema=blank',
                        'type' => 'blank',
                    ];
                    $output .= $this->objectRender->renderNodeHeader('schema', [], $more_attr_schema, false);
                    $more_attr_schema = [
                        'xlink_resource' => $this->wsUrl . $resourceName . '?schema=synopsis',
                        'type' => 'synopsis',
                    ];
                    $output .= $this->objectRender->renderNodeHeader('schema', [], $more_attr_schema, false);
                }
                $output .= $this->objectRender->renderNodeFooter($resourceName, []);
            }
        }
        $output .= $this->objectRender->renderNodeFooter('api', []);
        $output = $this->objectRender->overrideContent($output);

        return $output;
    }

    public function registerOverrideWSParameters($wsrObject, $method)
    {
        $this->wsParamOverrides[] = ['object' => $wsrObject, 'method' => $method];
    }

    /**
     * Method is used for each content type
     * Different content types are :
     * - list of entities,
     * - tree diagram of entity details (full or minimum),
     * - schema (synopsis & blank),.
     *
     * @param array $objects each object created by entity asked
     *
     *        @see WebserviceOutputBuilder::executeEntityGetAndHead
     *
     * @param string|null $schema_to_display if null display the entities list or entity details
     * @param string|array $fields_to_display the fields allow for the output
     * @param int $depth depth for the tree diagram output
     * @param int $type_of_view use the 2 constants WebserviceOutputBuilder::VIEW_LIST WebserviceOutputBuilder::VIEW_DETAILS
     *
     * @return string in the output format specified by WebserviceOutputBuilder::objectRender
     */
    public function getContent($objects, $schema_to_display = null, $fields_to_display = 'minimum', $depth = 0, $type_of_view = self::VIEW_LIST, $override = true)
    {
        $this->fieldsToDisplay = $fields_to_display;
        $this->depth = $depth;
        $output = '';

        if ($schema_to_display != null) {
            $this->schemaToDisplay = $schema_to_display;
            $this->objectRender->setSchemaToDisplay($this->schemaToDisplay);

            // If a shema is asked the view must be an details type
            $type_of_view = self::VIEW_DETAILS;
        }

        $class = get_class($objects['empty']);
        if (!isset(WebserviceOutputBuilder::$_cache_ws_parameters[$class])) {
            WebserviceOutputBuilder::$_cache_ws_parameters[$class] = $objects['empty']->getWebserviceParameters();
        }
        $ws_params = WebserviceOutputBuilder::$_cache_ws_parameters[$class];

        foreach ($this->wsParamOverrides as $p) {
            $object = $p['object'];
            $ws_params = $object->{$p['method']}($ws_params);
        }

        // If a list is asked, need to wrap with a plural node
        if ($type_of_view === self::VIEW_LIST) {
            $output .= $this->setIndent($depth) . $this->objectRender->renderNodeHeader($ws_params['objectsNodeName'], $ws_params);
        }

        if (null === $this->schemaToDisplay) {
            foreach ($objects as $key => $object) {
                if ($key !== 'empty') {
                    if ($this->fieldsToDisplay === 'minimum') {
                        $output .= $this->renderEntityMinimum($object, $depth);
                    } else {
                        $output .= $this->renderEntity($object, $depth);
                    }
                }
            }
        } else {
            $output .= $this->renderSchema($objects['empty'], $ws_params);
        }

        // If a list is asked, need to wrap with a plural node
        if ($type_of_view === self::VIEW_LIST) {
            $output .= $this->setIndent($depth) . $this->objectRender->renderNodeFooter($ws_params['objectsNodeName'], $ws_params);
        }

        if ($override) {
            $output = $this->objectRender->overrideContent($output);
        }

        return $output;
    }

    /**
     * Create the tree diagram with no details.
     *
     * @param ObjectModel $object create by the entity
     * @param int $depth the depth for the tree diagram
     *
     * @return string
     */
    public function renderEntityMinimum($object, $depth)
    {
        $class = get_class($object);
        if (!isset(WebserviceOutputBuilder::$_cache_ws_parameters[$class])) {
            WebserviceOutputBuilder::$_cache_ws_parameters[$class] = $object->getWebserviceParameters();
        }
        $ws_params = WebserviceOutputBuilder::$_cache_ws_parameters[$class];

        $more_attr['id'] = $object->id;
        $more_attr['xlink_resource'] = $this->wsUrl . $ws_params['objectsNodeName'] . '/' . $object->id;
        $output = $this->setIndent($depth) . $this->objectRender->renderNodeHeader($ws_params['objectNodeName'], $ws_params, $more_attr, false);

        return $output;
    }

    /**
     * Build a schema blank or synopsis.
     *
     * @param ObjectModel $object create by the entity
     * @param array $ws_params webserviceParams from the entity
     *
     * @return string
     */
    protected function renderSchema($object, $ws_params)
    {
        $output = $this->objectRender->renderNodeHeader($ws_params['objectNodeName'], $ws_params);
        foreach ($ws_params['fields'] as $field_name => $field) {
            $output .= $this->renderField($object, $ws_params, $field_name, $field, 0);
        }
        if (isset($ws_params['associations']) && count($ws_params['associations']) > 0) {
            $this->fieldsToDisplay = 'full';
            $output .= $this->renderAssociations($object, 0, $ws_params['associations'], $ws_params);
        }
        $output .= $this->objectRender->renderNodeFooter($ws_params['objectNodeName'], $ws_params);

        return $output;
    }

    /**
     * Build the entity detail.
     *
     * @param ObjectModel $object create by the entity
     * @param int $depth the depth for the tree diagram
     *
     * @return string
     */
    public function renderEntity($object, $depth)
    {
        $output = '';

        $class = get_class($object);
        if (!isset(WebserviceOutputBuilder::$_cache_ws_parameters[$class])) {
            WebserviceOutputBuilder::$_cache_ws_parameters[$class] = $object->getWebserviceParameters();
        }
        $ws_params = WebserviceOutputBuilder::$_cache_ws_parameters[$class];

        foreach ($this->wsParamOverrides as $p) {
            $o = $p['object'];
            $ws_params = $o->{$p['method']}($ws_params);
        }
        $output .= $this->setIndent($depth) . $this->objectRender->renderNodeHeader($ws_params['objectNodeName'], $ws_params);

        if ($object->id != 0) {
            // This to add virtual Fields for a particular entity.
            $virtual_fields = $this->addVirtualFields($ws_params['objectsNodeName'], $object);
            if (!empty($virtual_fields)) {
                $ws_params['fields'] = array_merge($ws_params['fields'], $virtual_fields);
            }

            foreach ($ws_params['fields'] as $field_name => $field) {
                if ($this->fieldsToDisplay === 'full' || array_key_exists($field_name, $this->fieldsToDisplay)) {
                    $field['object_id'] = $object->id;
                    $field['entity_name'] = $ws_params['objectNodeName'];
                    $field['entities_name'] = $ws_params['objectsNodeName'];
                    $output .= $this->renderField($object, $ws_params, $field_name, $field, $depth);
                }
            }
        }
        $subexists = false;
        if (is_array($this->fieldsToDisplay)) {
            foreach ($this->fieldsToDisplay as $fields) {
                if (is_array($fields)) {
                    $subexists = true;
                }
            }
        }

        if (isset($ws_params['associations'])
            && ($this->fieldsToDisplay == 'full'
            || $subexists)) {
            $output .= $this->renderAssociations($object, $depth, $ws_params['associations'], $ws_params);
        }

        $output .= $this->setIndent($depth) . $this->objectRender->renderNodeFooter($ws_params['objectNodeName'], $ws_params);

        return $output;
    }

    /**
     * Build a field and use recursivity depend on the depth parameter.
     *
     * @param ObjectModel $object create by the entity
     * @param array $ws_params webserviceParams from the entity
     * @param string $field_name
     * @param array $field
     * @param int $depth
     *
     * @return string
     */
    protected function renderField($object, $ws_params, $field_name, $field, $depth)
    {
        $output = '';
        $show_field = true;

        if (isset($ws_params['hidden_fields']) && in_array($field_name, $ws_params['hidden_fields'])) {
            return;
        }

        if ($this->schemaToDisplay === 'synopsis') {
            $field['synopsis_details'] = $this->getSynopsisDetails($field);
            if ($field_name === 'id') {
                $show_field = false;
            }
        }
        if ($this->schemaToDisplay === 'blank') {
            if (isset($field['setter']) && !$field['setter']) {
                $show_field = false;
            }
        }

        // don't set any value for a schema
        if (isset($field['synopsis_details']) || $this->schemaToDisplay === 'blank') {
            $field['value'] = '';
            if (isset($field['xlink_resource'])) {
                unset($field['xlink_resource']);
            }
        } elseif (isset($field['getter']) && $object != null && method_exists($object, $field['getter'])) {
            $field['value'] = $object->{$field['getter']}();
        } elseif (!isset($field['value'])) {
            $field['value'] = $object->$field_name;
        }

        // this apply specific function for a particular field on a choosen entity
        $field = $this->overrideSpecificField($ws_params['objectsNodeName'], $field_name, $field, $object, $ws_params);

        // don't display informations for a not existant id
        if (substr($field['sqlId'], 0, 3) == 'id_' && !$field['value']) {
            if ($field['value'] === null) {
                $field['value'] = '';
            }
            // delete the xlink except for schemas
            if (isset($field['xlink_resource']) && null === $this->schemaToDisplay) {
                unset($field['xlink_resource']);
            }
        }
        // set "id" for each node name which display the id of the entity
        if ($field_name === 'id') {
            $field['sqlId'] = 'id';
        }

        // don't display the node id for a synopsis schema
        if ($show_field) {
            $output .= $this->setIndent($depth - 1) . $this->objectRender->renderField($field);
        }

        return $output;
    }

    /**
     * @param $object
     * @param $depth
     * @param $associations
     * @param $ws_params
     *
     * @return string
     */
    protected function renderAssociations($object, $depth, $associations, $ws_params)
    {
        $output = $this->objectRender->renderAssociationWrapperHeader();
        foreach ($associations as $assoc_name => $association) {
            if ($this->fieldsToDisplay == 'full' || is_array($this->fieldsToDisplay) && array_key_exists($assoc_name, $this->fieldsToDisplay)) {
                $getter = $association['getter'];
                $objects_assoc = [];

                $fields_assoc = [];
                if (isset($association['fields'])) {
                    $fields_assoc = $association['fields'];
                }

                $parent_details = [
                    'object_id' => $object->id,
                    'entity_name' => $ws_params['objectNodeName'],
                    'entities_name' => $ws_params['objectsNodeName'],
                ];

                if (is_array($getter)) {
                    $association_resources = call_user_func($getter, $object);
                    if (is_array($association_resources) && !empty($association_resources)) {
                        foreach ($association_resources as $association_resource) {
                            $objects_assoc[] = $association_resource;
                        }
                    }
                } else {
                    if (method_exists($object, $getter) && null === $this->schemaToDisplay) {
                        $association_resources = $object->$getter();
                        if (is_array($association_resources) && !empty($association_resources)) {
                            foreach ($association_resources as $association_resource) {
                                $objects_assoc[] = $association_resource;
                            }
                        }
                    } else {
                        $objects_assoc[] = '';
                    }
                }

                $class_name = null;
                if (isset($this->wsResource[$assoc_name]['class']) && class_exists($this->wsResource[$assoc_name]['class'], true)) {
                    $class_name = $this->wsResource[$assoc_name]['class'];
                }
                $output_details = '';
                foreach ($objects_assoc as $object_assoc) {
                    if ($depth == 0 || $class_name === null) {
                        $value = null;
                        if (!empty($object_assoc)) {
                            $value = $object_assoc;
                        }
                        if (empty($fields_assoc)) {
                            $fields_assoc = [['id' => $value['id']]];
                        }
                        $output_details .= $this->renderFlatAssociation($object, $depth, $assoc_name, $association['resource'], $fields_assoc, $value, $parent_details);
                    } else {
                        foreach ($object_assoc as $id) {
                            if ($class_name !== null) {
                                $child_object = new $class_name($id);
                                $output_details .= $this->renderEntity($child_object, ($depth - 2 ? 0 : $depth - 2));
                            }
                        }
                    }
                }
                if ($output_details != '') {
                    $output .= $this->setIndent($depth) . $this->objectRender->renderAssociationHeader($object, $ws_params, $assoc_name);
                    $output .= $output_details;
                    $output .= $this->setIndent($depth) . $this->objectRender->renderAssociationFooter($object, $ws_params, $assoc_name);
                } else {
                    $output .= $this->setIndent($depth) . $this->objectRender->renderAssociationHeader($object, $ws_params, $assoc_name, true);
                }
            }
        }
        $output .= $this->objectRender->renderAssociationWrapperFooter();

        return $output;
    }

    protected function renderFlatAssociation($object, $depth, $assoc_name, $resource_name, $fields_assoc, $object_assoc, $parent_details)
    {
        $output = '';
        $more_attr = [];
        if (isset($this->wsResource[$assoc_name]) && null === $this->schemaToDisplay) {
            if ($assoc_name == 'images') {
                if ($parent_details['entities_name'] == 'combinations') {
                    $more_attr['xlink_resource'] = $this->wsUrl . $assoc_name . '/products/' . $object->id_product . '/' . $object_assoc['id'];
                } else {
                    $more_attr['xlink_resource'] = $this->wsUrl . $assoc_name . '/' . $parent_details['entities_name'] . '/' . $parent_details['object_id'] . '/' . $object_assoc['id'];
                }
            } else {
                $more_attr['xlink_resource'] = $this->wsUrl . $assoc_name . '/' . $object_assoc['id'];
            }
        }
        $output .= $this->setIndent($depth - 1) . $this->objectRender->renderNodeHeader($resource_name, [], $more_attr);

        foreach ($fields_assoc as $field_name => $field) {
            if (!is_array($this->fieldsToDisplay) || in_array($field_name, $this->fieldsToDisplay[$assoc_name])) {
                if ($field_name == 'id' && !isset($field['sqlId'])) {
                    $field['sqlId'] = 'id';
                    $field['value'] = $object_assoc['id'];
                } elseif (!isset($field['sqlId'])) {
                    $field['sqlId'] = $field_name;
                    $field['value'] = $object_assoc[$field_name];
                }
                $field['entities_name'] = $assoc_name;
                $field['entity_name'] = $resource_name;

                if (null !== $this->schemaToDisplay) {
                    $field['synopsis_details'] = $this->getSynopsisDetails($field);
                }
                $field['is_association'] = true;
                $output .= $this->setIndent($depth - 1) . $this->objectRender->renderField($field);
            }
        }
        $output .= $this->setIndent($depth - 1) . $this->objectRender->renderNodeFooter($resource_name, []);

        return $output;
    }

    public function setIndent($depth)
    {
        $string = '';
        $number_of_tabs = $this->depth - $depth;
        for ($i = 0; $i < $number_of_tabs; ++$i) {
            $string .= "\t";
        }

        return $string;
    }

    public function getSynopsisDetails($field)
    {
        $arr_details = [];
        if (array_key_exists('required', $field) && $field['required']) {
            $arr_details['required'] = 'true';
        }
        if (array_key_exists('maxSize', $field) && $field['maxSize']) {
            $arr_details['maxSize'] = $field['maxSize'];
        }
        if (array_key_exists('validateMethod', $field) && $field['validateMethod']) {
            $arr_details['format'] = $field['validateMethod'];
        }
        if (array_key_exists('setter', $field) && !$field['setter']) {
            $arr_details['readOnly'] = 'true';
        }

        return $arr_details;
    }

    /**
     * @param string|object $object
     * @param string $method
     * @param $field_name
     * @param $entity_name
     *
     * @return WebserviceOutputBuilder
     *
     * @throws Exception
     * @throws WebserviceException
     */
    public function setSpecificField($object, $method, $field_name, $entity_name)
    {
        try {
            $this->validateObjectAndMethod($object, $method);
        } catch (WebserviceException $e) {
            throw $e;
        }

        $this->specificFields[$field_name] = ['entity' => $entity_name, 'object' => $object, 'method' => $method, 'type' => gettype($object)];

        return $this;
    }

    protected function validateObjectAndMethod($object, $method)
    {
        if (is_string($object) && !class_exists($object)) {
            throw new WebserviceException('The object you want to set in ' . __METHOD__ . ' is not allowed.', [98, 500]);
        }
        if (!method_exists($object, $method)) {
            throw new WebserviceException('The method you want to set in ' . __METHOD__ . ' is not allowed.', [99, 500]);
        }
    }

    public function getSpecificField()
    {
        return $this->specificFields;
    }

    protected function overrideSpecificField($entity_name, $field_name, $field, $entity_object, $ws_params)
    {
        if (array_key_exists($field_name, $this->specificFields) && $this->specificFields[$field_name]['entity'] == $entity_name) {
            if ($this->specificFields[$field_name]['type'] == 'string') {
                $object = new $this->specificFields[$field_name]['object']();
            } elseif ($this->specificFields[$field_name]['type'] == 'object') {
                $object = $this->specificFields[$field_name]['object'];
            }

            $field = $object->{$this->specificFields[$field_name]['method']}($field, $entity_object, $ws_params);
        }

        return $field;
    }

    public function setVirtualField($object, $method, $entity_name, $parameters)
    {
        try {
            $this->validateObjectAndMethod($object, $method);
        } catch (WebserviceException $e) {
            throw $e;
        }

        $this->virtualFields[$entity_name][] = ['parameters' => $parameters, 'object' => $object, 'method' => $method, 'type' => gettype($object)];
    }

    public function getVirtualFields()
    {
        return $this->virtualFields;
    }

    public function addVirtualFields($entity_name, $entity_object)
    {
        $arr_return = [];
        $virtual_fields = $this->getVirtualFields();
        if (array_key_exists($entity_name, $virtual_fields)) {
            foreach ($virtual_fields[$entity_name] as $function_infos) {
                if ($function_infos['type'] == 'string') {
                    $object = new $function_infos['object']();
                } elseif ($function_infos['type'] == 'object') {
                    $object = $function_infos['object'];
                }

                $return_fields = $object->{$function_infos['method']}($entity_object, $function_infos['parameters']);
                foreach ($return_fields as $field_name => $value) {
                    if (Validate::isConfigName($field_name)) {
                        $arr_return[$field_name] = $value;
                    } else {
                        throw new WebserviceException('Name for the virtual field is not allow', [128, 400]);
                    }
                }
            }
        }

        return $arr_return;
    }

    public function setFieldsToDisplay($fields)
    {
        $this->fieldsToDisplay = $fields;
    }
}
